package ru.spbau.database.group3.engine;


import ru.spbau.database.group3.filesAndAccess.HeapFile;
import ru.spbau.database.group3.filesAndAccess.Record;
import ru.spbau.database.group3.index.BTreeIndex;
import ru.spbau.database.group3.index.HashIndex;
import ru.spbau.database.group3.index.IndexKeyType;
import ru.spbau.database.group3.memoryManager.SpaceManager;
import ru.spbau.database.group3.parser.ConditionOperator;
import ru.spbau.database.group3.parser.Constraint;
import ru.spbau.database.group3.queryEvaluator.OKResult;
import ru.spbau.database.group3.queryEvaluator.QueryResult;
import ru.spbau.database.group3.queryEvaluator.SelectResult;
import ru.spbau.database.group3.types.IntegerType;
import ru.spbau.database.group3.types.Type;

import java.io.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * Created with IntelliJ IDEA.
 * User: stels
 * Date: 10.02.13
 * Time: 22:33
 */
public class Relation implements Serializable {


    public Relation(String relationName, List<Column> columns, int recordSize) throws IOException {
        this.relationName = relationName;
        this.columns = columns;
        this.recordSize = recordSize;
        this.hashIndexes = new ArrayList<HashIndexInfo>();
        this.btreeIndexes = new ArrayList<BTreeIndexInfo>();
        dump(new HeapFile(relationName, columns, new LinkedList<Integer>(), new LinkedList<Integer>()));
    }

    public QueryResult insertRequest(List<Object> recordValues) throws IOException, ClassNotFoundException {
        ByteBuffer recordData = createRecord(recordValues);
        HeapFile heapFile = fetchHeapFile();
        Record r = heapFile.insertRecord(recordData);
        dump(heapFile);
        for (HashIndexInfo hashIndexInfo : hashIndexes) {
            HashIndex hashIndex = fetchHashIndex(hashIndexInfo.getIndexName());
            hashIndex.insert(createEntry(hashIndexInfo, r));
            dump(hashIndex);
        }
        for (BTreeIndexInfo btreeIndexInfo : btreeIndexes) {
        	BTreeIndex treeIndex = fetchBtreeIndex(btreeIndexInfo.getIndexName());
//        	System.out.println(treeIndex);
        	treeIndex.insertRecord(r);
        	dump(treeIndex);
//        	System.out.println(treeIndex);
        }
        return new OKResult();
    }


    //private Map<IndexKeyType, Index> indicesMap = new HashMap<IndexKeyType, Index>();
//
//    private Index getIndexForConditions(Collection<Condition> conditions) {
//    	Set<Column> conditionedColumns = new HashSet<Column>();
//    	for (Condition c: conditions) {
//    		conditionedColumns.add(c.getColumn());
//    	}
//    	IndexKeyType indexKeyType = new IndexKeyType(conditionedColumns);
//    	return indicesMap.get(indexKeyType);
//    }
//
//    private Index getIndexForCondition(Condition c) {
//    	//XXX: condition covers only single-column conditions; multi-indices are useless
//    	IndexKeyType indexKeyType = new IndexKeyType(c.getColumn());
//    	return indicesMap.get(indexKeyType);
//    }

    public QueryResult selectRequest(List<String> colNames, List<Condition> conditions) throws ClassNotFoundException, IOException {
        if (hasSuitableHashIndex(conditions)) {
            HashIndexInfo hashIndexInfo = findSuitableIndex(conditions);
            HashIndex hashIndex = fetchHashIndex(hashIndexInfo.getIndexName());
            List<Condition> searchKey = hashIndexInfo.createSearchKey(conditions);
            return new SelectResult(hashIndex.select(searchKey, conditions), columns);
        }
        BTreeIndexInfo bTreeIndexInfo = getSuitableBtreeIndex(conditions);
        if (bTreeIndexInfo != null) {
        	BTreeIndex treeIndex = fetchBtreeIndex(bTreeIndexInfo.getIndexName());
        	return new SelectResult(treeIndex.getRecords(conditions), columns);
        }
        HeapFile heapFile = fetchHeapFile();
        return new SelectResult(heapFile.selectRecords(conditions), columns);
    }

    public QueryResult deleteRequest(List<Condition> conditions) throws ClassNotFoundException, IOException {
        List<Record> records = null;
        HeapFile heapFile = fetchHeapFile();
        for (HashIndexInfo hashIndexInfo : hashIndexes) {
            HashIndex hashIndex = fetchHashIndex(hashIndexInfo.getIndexName());
            List<Condition> searchKey = hashIndexInfo.createSearchKey(conditions);
            records = hashIndex.delete(searchKey, conditions);
            dump(hashIndex);
        }
        for (BTreeIndexInfo btreeIndexInfo : btreeIndexes) {
            BTreeIndex treeIndex = fetchBtreeIndex(btreeIndexInfo.getIndexName());
            records = (List<Record>)treeIndex.getRecords(conditions);
            for (Record r : records){
                treeIndex.deleteRecord(r);
            }
            dump(treeIndex);
        }
        records =  heapFile.deleteRecords(conditions);
        dump(heapFile);
        return new OKResult();
    }

    private String checkColumns(List<String> cols){
        for (String colName : cols){
            boolean flag = true;
            for (Column c : columns){
                if (c.getName().equals(colName)){
                    flag = false;
                    break;
                }
            }
            if (flag){
                return colName;
            }
        }
        return null;
    }

    public QueryResult updateRequest(List<Condition> conditions, List<String> cols, List<Object> values)
            throws ClassNotFoundException, IOException {
        String wrong = checkColumns(cols);
        if (wrong != null){
            throw  new IllegalArgumentException("no such column: "+wrong+" in requested table");
        }
        List<Record> records = null;
        HeapFile heapFile = fetchHeapFile();
        for (HashIndexInfo hashIndexInfo : hashIndexes) {
            HashIndex hashIndex = fetchHashIndex(hashIndexInfo.getIndexName());
            List<Condition> searchKey = hashIndexInfo.createSearchKey(conditions);
            records = hashIndex.delete(searchKey, conditions);
            dump(hashIndex);
        }
        for (BTreeIndexInfo btreeIndexInfo : btreeIndexes) {
            BTreeIndex treeIndex = fetchBtreeIndex(btreeIndexInfo.getIndexName());
            records = (List<Record>)treeIndex.getRecords(conditions);
            for (Record r : records){
                treeIndex.deleteRecord(r);
            }
            dump(treeIndex);
        }
        records =  heapFile.deleteRecords(conditions);
        dump(heapFile);
        for (Record r : records){
            List<Object> newRecordValues = new ArrayList<Object>();
            for (Column c : columns){
                if (cols.contains(c.getName())){
                    newRecordValues.add(values.get(cols.indexOf(c.getName())));

                } else{
                    newRecordValues.add(r.getColumnValue(c));
                }
            }
            insertRequest(newRecordValues);
        }
        return new OKResult();
    }


    public QueryResult createBtreeIndexRequest(String indexName, List<String> columnNames, List<String> orders, String type) throws IOException, ClassNotFoundException {
    	boolean isUnique = false;//type.toLowerCase().equals("unique");
    	int offset = 0;
    	List<Column> indexColumns = new LinkedList<Column>();
        for (String columnName : columnNames) {
            for (Column column : columns) {
                if (column.getName().equals(columnName)) {
                    indexColumns.add(new Column(columnName, offset, column.getType()));
                    offset += column.getType().size();
                }
            }
        }
        Set<Column> columnsSet = new HashSet<Column>();
        columnsSet.addAll(indexColumns);
        SpaceManager.createIndex(relationName + indexName + DATA_EXT);
        BTreeIndexInfo indexInfo = new BTreeIndexInfo(new IndexKeyType(columnsSet, columns), indexName);
        BTreeIndex index = new BTreeIndex(columnsSet, isUnique, relationName + indexName, DatabaseEngine.calculateRecordSize(indexColumns), columns);
        //TODO: create the actual index files
        HeapFile heapFile = fetchHeapFile();
        for (Record r : heapFile.selectRecords(new LinkedList<Condition>())) {
        	index.insertRecord(r);
        }
        btreeIndexes.add(indexInfo);
        dump(index);
    	return new OKResult();
    }
    
    public QueryResult createHashIndexRequest(String indexName, List<String> columnNames, List<String> orders, String type) throws IOException, ClassNotFoundException {
        List<Column> indexColumns = createIndexColumns(columnNames, orders);
        SpaceManager.createIndex(relationName + indexName + DATA_EXT);
        HashIndexInfo hashIndexInfo = new HashIndexInfo(indexName, DatabaseEngine.calculateRecordSize(indexColumns), indexColumns);
        HashIndex hashIndex = new HashIndex(relationName + indexName, hashIndexInfo.getIndexEntrySize(), indexColumns);
        HeapFile heapFile = fetchHeapFile();
        for (Record r : heapFile.selectRecords(new LinkedList<Condition>())) {
            hashIndex.insert(createEntry(hashIndexInfo, r));
        }
        hashIndexes.add(hashIndexInfo);
        dump(hashIndex);
        return new OKResult();
    }

    public Condition createCondition(Constraint constraint) {
        Column column = getColumn(constraint.getColName());
        Object value = column.getType().valueOf(constraint.getValue());
        ConditionOperator cOp = constraint.getConditionOperator();
        return new Condition(column, value, cOp);

    }

    public final static String META_EXT = ".m";
    public final static String DATA_EXT = ".d";

//    private boolean hasSuitableIndex(List<Condition> c) {
//        return getIndexForCondition(c.get(0)) != null;
//    }


    private HashIndexInfo findSuitableIndex(List<Condition> conditions) {
        for (HashIndexInfo hii : hashIndexes) {
            if (hii.isSuitable(conditions)) {
                return hii;
            }
        }
        throw new IllegalArgumentException("There are no index suitable for these conditions");
    }

    private BTreeIndexInfo getSuitableBtreeIndex(List<Condition> conditions) {
    	for (BTreeIndexInfo bti : btreeIndexes) {
    		if (bti.isSuitable(conditions, columns)) {
    			return bti;
    		}
    	}
    	return null;
    }
    
    private boolean hasSuitableHashIndex(List<Condition> conditions) {
        for (HashIndexInfo hii : hashIndexes) {
            if (hii.isSuitable(conditions)) {
                return true;
            }
        }
        return false;
    }

    private void dump(HeapFile heapFile) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(relationName + "HeapFile" + Relation.META_EXT)));
        oos.writeObject(heapFile);
        oos.flush();
        oos.close();
    }

    private void dump(HashIndex hashIndex) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(hashIndex.getPath() + Relation.META_EXT)));
        oos.writeObject(hashIndex);
        oos.flush();
        oos.close();
    }
    
    private void dump(BTreeIndex treeIndex) throws FileNotFoundException, IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(treeIndex.getName() + META_EXT)));
        oos.writeObject(treeIndex);
        oos.flush();
        oos.close();    	
    }

    private HashIndex fetchHashIndex(String indexName) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(relationName + indexName + META_EXT)));
        HashIndex hashIndex = (HashIndex) ois.readObject();
        ois.close();
        return hashIndex;
    }

    private BTreeIndex fetchBtreeIndex(String indexName) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(relationName + indexName + META_EXT)));
        BTreeIndex btreeIndex = (BTreeIndex) ois.readObject();
        ois.close();
        return btreeIndex;    	
    }

    
    private HeapFile fetchHeapFile() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(relationName + "HeapFile" + META_EXT)));
        HeapFile heapFile = (HeapFile) ois.readObject();
        ois.close();
        return heapFile;
    }

    private Column getColumn(String colName) {
        for (Column c : columns) {
            if (colName.equals(c.getName())) {
                return c;
            }
        }
        throw new IllegalArgumentException("Wrong Column Name " + colName);
    }

    private ByteBuffer createRecord(List<Object> values) {
        ByteBuffer recordData = ByteBuffer.allocate(recordSize);
        for (int i = 0; i < columns.size(); ++i) {
            columns.get(i).getType().writeValueToByteBuffer(recordData, values.get(i));
        }
        return recordData;
    }


    private ByteBuffer createEntry(HashIndexInfo hashIndexInfo, Record r) throws IOException {
        ByteBuffer entryData = ByteBuffer.allocate(hashIndexInfo.getIndexEntrySize());
        for (Column indexColumn : hashIndexInfo.getKeyColumns()) {
            for (Column column : columns) {
                if (column.getName().equals(indexColumn.getName())) {
                    indexColumn.getType().writeValueToByteBuffer(entryData, r.getColumnValue(column));
                }
            }
        }
        entryData.putInt(r.getPid());
        entryData.putInt(r.getSid());
        return entryData;
    }

    private List<Column> createIndexColumns(List<String> columnNames, List<String> orders) {
        List<Column> indexColumns = new ArrayList<Column>();
        int offset = 0;
        for (String columnName : columnNames) {
            for (Column column : columns) {
                if (column.getName().equals(columnName)) {
                    indexColumns.add(new Column(columnName, offset, column.getType()));
                    offset += column.getType().size();
                }
            }
        }
        return addPointerToRecordColumns(indexColumns, offset);
    }

    private List<Column> addPointerToRecordColumns(List<Column> indexColumns, int offset) {
        Column pid = new Column("pid", offset, new IntegerType());
        Column sid = new Column("sid", offset + Type.INTEGER_SIZE, new IntegerType());
        indexColumns.add(pid);
        indexColumns.add(sid);
        return indexColumns;
    }

    private final String relationName;
    private List<Column> columns;
    private List<HashIndexInfo> hashIndexes;
    private List<BTreeIndexInfo> btreeIndexes;
    private int recordSize;


//    public QueryResult updateRequest(ArrayList<String> columnNames, ArrayList<String> values,Condition c) throws IOException, ClassNotFoundException {
//        TableData tableData = getTableData(c);
//        Collection<ByteBuffer> recordData = new ArrayList<ByteBuffer>();
//        Collection<Record> updateRecords = tableData.deleteRecords(c);
//        QueryResult queryResult = generateDeleteResult(updateRecords);
//        if (!(queryResult instanceof OKResult)){
//            return queryResult;
//        }
//        for (Record rec : updateRecords){
//            ArrayList<String> newValues = new ArrayList<String>();
//            for (Column col : columns){
//                int idx = -1;
//                if ((idx = columnNames.indexOf(col.getName())) != -1){
//                    newValues.add(values.get(idx));
//                } else{
//                    newValues.add((String)rec.getColumnValue(col));
//                }
//            }
//            recordData.add(createRecordData(recordSize,newValues));
//        }
//        ByteBuffer[] arr = new ByteBuffer[recordData.size()];
//        return generateBulkInsertResult(tableData.bulkInsertRecords(recordData.toArray(arr)));
//    }
//
//    private ByteBuffer createRecordData(int recordSize, ArrayList<String> values) {
//        ByteBuffer recordData = ByteBuffer.allocate(recordSize);
//        for(String value : values) {
//            putValue(recordData, value);
//        }
//        return recordData;
//    }
//
//    private void putValue(ByteBuffer recordData, String value) {
//        if(value.matches("\".*\"")) {
//            putString(recordData, value.substring(1, value.length()-1));
//        } else {
//            if(value.indexOf('.') > 0) {
//                recordData.putDouble(Double.parseDouble(value));
//            } else {
//                recordData.putInt(Integer.parseInt(value));
//            }
//        }
//    }
//
//    private void putString(ByteBuffer recordData, String value) {
//        for(int i = 0; i < value.length(); ++i) {
//            recordData.putChar(value.charAt(i));
//        }
//    }

//    private int calculateSize(ArrayList<Column> columns) {
//        int size = 0;
//        for(Column c : columns) {
//            size += c.getType().size();
//        }
//        return size;
//    }

//

}
